package com.seventhdawn.gwt.rcx.server;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gwt.user.client.rpc.IsSerializable;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.server.rpc.RPC;
import com.google.gwt.user.server.rpc.RPCServletUtils;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.google.gwt.user.server.rpc.SerializationPolicy;
import com.google.gwt.user.server.rpc.SerializationPolicyLoader;
import com.google.gwt.user.server.rpc.SerializationPolicyProvider;
import com.google.gwt.user.server.rpc.impl.ServerSerializationStreamReader;
import com.seventhdawn.gwt.rcx.client.RPCClientContext;
import com.seventhdawn.gwt.rcx.client.RPCContextServiceProxy;

/**
 * Filter that handles RPC context object deserialization prior to
 * processing an RPC call.
 * <p>
 * <b>Important notice:</b> This filter needs to be applied <em>before
 * your server-side RPC service implementation</em> (e.g. a
 * {@link RemoteServiceServlet} subclass) takes over the RPC request
 * processing.
 * <p>
 * RPC service methods can access the deserialized context object
 * via {@link RPCServerContext}.
 * 
 * @see RPCClientContext
 * @see RPCServerContext
 * @see RPCContextServiceProxy
 * @author vojtech.szocs
 */
public class RPCContextFilter implements Filter, SerializationPolicyProvider {
    
    private static final Logger LOG = LoggerFactory.getLogger(RPCContextFilter.class);
    
    /**
     * Name of the initialization parameter that specifies whether a missing RPC
     * context payload (indicated by a special
     * {@link RPCContextServiceProxy#CONTEXT_PAYLOAD_LENGTH header}) should be
     * ignored.
     * <p>
     * Possible values: <tt>true</tt>, <tt>false</tt>. Default is <tt>false</tt>.
     * <p>
     * This parameter is optional.
     */
    public static final String IGNORE_MISSING_PAYLOAD = "ignoreMissingPayload";
    
    /**
     * Name of the initialization parameter that specifies whether serialization
     * errors during RPC context object deserialization should be ignored.
     * <p>
     * Possible values: <tt>true</tt>, <tt>false</tt>. Default is <tt>false</tt>.
     * <p>
     * This parameter is optional.
     */
    public static final String IGNORE_SERIALIZATION_ERRORS = "ignoreSerializationErrors";
    
    private FilterConfig filterConfig;
    
    protected boolean getBooleanInitParam(String paramName, boolean defaultValue) {
        String paramValue = filterConfig.getInitParameter(paramName);
        
        if (paramValue != null)
            return Boolean.valueOf(paramValue);
        else
            return defaultValue;
    }
    
    /**
     * RPC context object deserialization logic.
     */
    protected void deserializeContextObject(String contextPayload,
            boolean ignoreSerializationErrors) throws ServletException {
        try {
            ServerSerializationStreamReader streamReader = new ServerSerializationStreamReader(
                    Thread.currentThread().getContextClassLoader(), this);
            streamReader.prepareToRead(contextPayload);
            IsSerializable contextObject = (IsSerializable) streamReader.readObject();
            RPCServerContext.setContextObject(contextObject);
        } catch (SerializationException sex) {
            if (ignoreSerializationErrors) {
                LOG.warn("Ignoring RPC context object deserialization error", sex);
            } else {
                throw new ServletException("Failed to deserialize RPC context object", sex);
            }
        }
    }
    
    /**
     * Process RPC context payload that comes along the given HTTP request,
     * returning a request instance to be passed to the next entity in the
     * filter chain.
     */
    protected HttpServletRequest processPayload(HttpServletRequest request)
            throws IOException, ServletException {
        boolean ignoreMissingPayload = getBooleanInitParam(IGNORE_MISSING_PAYLOAD, false);
        boolean ignoreSerializationErrors = getBooleanInitParam(IGNORE_SERIALIZATION_ERRORS, false);
        
        // extract RPC context payload
        String contextPayloadLength = request.getHeader(RPCContextServiceProxy.CONTEXT_PAYLOAD_LENGTH);
        if (contextPayloadLength == null) {
            if (ignoreMissingPayload) {
                LOG.warn("Ignoring missing RPC context payload");
                return request;
            } else {
                throw new ServletException("Missing RPC context payload");
            }
        }
        
        int contentLength = request.getContentLength();
        if (contentLength == -1) {
            throw new ServletException("Content-Length must be specified");
        }
        final int contextPayloadOffset = contentLength - Integer.valueOf(contextPayloadLength);
        
        String requestBody = RPCServletUtils.readContentAsUtf8(request, true);
        String contextPayload = requestBody.substring(contextPayloadOffset);
        
        // deserialize RPC context object
        deserializeContextObject(contextPayload, ignoreSerializationErrors);
        
        // split default RPC payload apart from RPC context payload
        byte[] defaultRpcPayload = requestBody.substring(0, contextPayloadOffset).getBytes();
        final ByteArrayInputStream defaultRpcInputStream = new ByteArrayInputStream(defaultRpcPayload);
        
        return new HttpServletRequestWrapper(request) {
            
            @Override
            public int getContentLength() {
                return contextPayloadOffset;
            }
            
            @Override
            public ServletInputStream getInputStream() throws IOException {
                return new ServletInputStream() {
                    
                    @Override
                    public int available() throws IOException {
                        return defaultRpcInputStream.available();
                    }
                    
                    @Override
                    public int read() throws IOException {
                        return defaultRpcInputStream.read();
                    }
                    
                    @Override
                    public int read(byte[] b, int off, int len) throws IOException {
                        return defaultRpcInputStream.read(b, off, len);
                    }
                    
                };
            }
            
        };
    }
    
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        if (filterConfig == null)
            throw new ServletException("doFilter() called before init()");
        
        try {
            // thread-local request for deserialization purposes
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            perThreadRequest.set(httpRequest);
            
            // filter incoming request and pass it along the filter chain
            HttpServletRequest filteredRequest = processPayload(httpRequest);
            chain.doFilter(filteredRequest, response);
        } finally {
            // nullify thread-local variables
            RPCServerContext.setContextObject(null);
            perThreadRequest.set(null);
        }
    }
    
    public void init(FilterConfig filterConfig) throws ServletException {
        this.filterConfig = filterConfig;
    }
    
    public void destroy() {
        filterConfig = null;
    }
    
    
    // following code is taken from RemoteServiceServlet
    
    private final ThreadLocal<HttpServletRequest> perThreadRequest = new ThreadLocal<HttpServletRequest>();
    
    /**
     * A cache of moduleBaseURL and serialization policy strong name to
     * {@link SerializationPolicy}.
     */
    private final Map<String, SerializationPolicy> serializationPolicyCache = new HashMap<String, SerializationPolicy>();
    
    public final SerializationPolicy getSerializationPolicy(
            String moduleBaseURL, String strongName) {
        
        SerializationPolicy serializationPolicy = getCachedSerializationPolicy(
                moduleBaseURL, strongName);
        if (serializationPolicy != null) {
            return serializationPolicy;
        }
        
        serializationPolicy = doGetSerializationPolicy(getThreadLocalRequest(),
                moduleBaseURL, strongName);
        
        if (serializationPolicy == null) {
            // Failed to get the requested serialization policy; use the default
            LOG.warn("Failed to get the SerializationPolicy '"
                    + strongName + "' for module '" + moduleBaseURL
                    + "'; a legacy, 1.3.3 compatible, serialization policy will be used."
                    + "  You may experience SerializationExceptions as a result.");
            serializationPolicy = RPC.getDefaultSerializationPolicy();
        }
        
        // This could cache null or an actual instance. Either way we will not
        // attempt to lookup the policy again.
        putCachedSerializationPolicy(moduleBaseURL, strongName, serializationPolicy);
        
        return serializationPolicy;
    }
    
    /**
     * Gets the {@link SerializationPolicy} for given module base URL and strong
     * name if there is one. Override this method to provide a
     * {@link SerializationPolicy} using an alternative approach.
     * 
     * @param request the HTTP request being serviced
     * @param moduleBaseURL as specified in the incoming payload
     * @param strongName a strong name that uniquely identifies a serialization
     * policy file
     * @return a {@link SerializationPolicy} for the given module base URL and
     * strong name, or <code>null</code> if there is none
     */
    protected SerializationPolicy doGetSerializationPolicy(
            HttpServletRequest request, String moduleBaseURL, String strongName) {
        // The request can tell you the path of the web app relative to the
        // container root.
        String contextPath = request.getContextPath();
        
        String modulePath = null;
        if (moduleBaseURL != null) {
            try {
                modulePath = new URL(moduleBaseURL).getPath();
            } catch (MalformedURLException ex) {
                // log the information, we will default
                LOG.warn("Malformed moduleBaseURL: " + moduleBaseURL, ex);
            }
        }
        
        SerializationPolicy serializationPolicy = null;
        
        /*
         * Check that the module path must be in the same web app as the servlet
         * itself. If you need to implement a scheme different than this,
         * override this method.
         */
        if (modulePath == null || !modulePath.startsWith(contextPath)) {
            String message = "The module path requested, "
                    + modulePath
                    + ", is not in the same web application as this servlet, "
                    + contextPath
                    + ".  Your module may not be properly configured or your client and server code maybe out of date.";
            LOG.error(message);
        } else {
            // Strip off the context path from the module base URL. It should be
            // a strict prefix.
            String contextRelativePath = modulePath.substring(contextPath.length());
            
            String serializationPolicyFilePath = SerializationPolicyLoader
                    .getSerializationPolicyFileName(contextRelativePath + strongName);
            
            // Open the RPC resource file read its contents.
            InputStream is = filterConfig.getServletContext().getResourceAsStream(
                    serializationPolicyFilePath);
            try {
                if (is != null) {
                    try {
                        serializationPolicy = SerializationPolicyLoader
                                .loadFromStream(is, null);
                    } catch (ParseException e) {
                        LOG.error("Failed to parse the policy file '"
                                + serializationPolicyFilePath + "'", e);
                    } catch (IOException e) {
                        LOG.error("Could not read the policy file '"
                                + serializationPolicyFilePath + "'", e);
                    }
                } else {
                    String message = "The serialization policy file '"
                            + serializationPolicyFilePath
                            + "' was not found; did you forget to include it in this deployment?";
                    LOG.error(message);
                }
            } finally {
                if (is != null) {
                    try {
                        is.close();
                    } catch (IOException e) {
                        // Ignore this error
                    }
                }
            }
        }
        
        return serializationPolicy;
    }
    
    /**
     * Gets the <code>HttpServletRequest</code> object for the current call. It
     * is stored thread-locally so that simultaneous invocations can have
     * different request objects.
     */
    protected final HttpServletRequest getThreadLocalRequest() {
        return perThreadRequest.get();
    }
    
    private SerializationPolicy getCachedSerializationPolicy(
            String moduleBaseURL, String strongName) {
        synchronized (serializationPolicyCache) {
            return serializationPolicyCache.get(moduleBaseURL + strongName);
        }
    }
    
    private void putCachedSerializationPolicy(String moduleBaseURL,
            String strongName, SerializationPolicy serializationPolicy) {
        synchronized (serializationPolicyCache) {
            serializationPolicyCache.put(moduleBaseURL + strongName, serializationPolicy);
        }
    }
    
}
